package flexcloud.controls
{

import flash.display.MovieClip;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.text.TextFormat;

import flexcloud.controls.cumulusClasses.Tag;
import flexcloud.events.TagEvent;

import mx.controls.listClasses.ListBase;
import mx.events.FlexEvent;

//--------------------------------------
//  Events
//--------------------------------------

/**
 * TBD
 * 
 * possible events :
 * 
 * mouseOverTag
 * mouseOutTag
 * clickTag
 * ...
 * 
 */

[Event(name="tagClick", type="flexcloud.events.TagEvent")]
[Event(name="tagRollOut", type="flexcloud.events.TagEvent")]
[Event(name="tagRollOver", type="flexcloud.events.TagEvent")]

//--------------------------------------
//  Styles
//--------------------------------------

/**
 * TBD
 * 
 * possible styles :
 * 
 * backgroundColor
 * labelColor
 * labelHoverColor
 * labelBackgroundColor
 * labelHoverBackgroundColor
 * labelMinFontSize
 * labelMaxFontSize
 * labelFontFace
 * ...
 * 
 */

//--------------------------------------
//  Excluded APIs
//--------------------------------------

// TBD

//--------------------------------------
//  Other metadata
//--------------------------------------

// TBC

//[DefaultProperty("dataProvider")]

public class Cumulus extends ListBase
{
	
	//[Embed(source="assets/ACaslonPro-Regular.otf",
    //                fontFamily="ArialEmbedded",
    //                mimeType="application/x-font",
    //                embedAsCFF="true")]
	//private const arialEmbedded:Class;
	
	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------
	
	public function Cumulus()
	{
		super();
		
		/* Tags are nested in movieClip to allow the use of setChildIndex which
		 * wouldn't be possible if tags (not extending UIComponent nor
		 * implementing IUIComponent) were immediate children of Cumulus.
		 */
		addChild(movieClip);
		
		// textFormat contains Tag format
		//textFormat.font = "ArialEmbedded";
		//textFormat.bold = true;
		textFormat.color = 0x000000;
		textFormat.size = 14;
		
		// handle Flash events
		addEventListener(Event.ENTER_FRAME, enterFrameHandler);
		addEventListener(MouseEvent.MOUSE_OUT, doStartAnimate );
		addEventListener(MouseEvent.MOUSE_WHEEL, doStartAnimate );
		addEventListener(MouseEvent.MOUSE_OVER, doStopAnimate);
		
		// handle Flex events
		addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);
		
		// handle UIComponent events
		addEventListener("widthChanged",  sizeChangedHandler);
		addEventListener("heightChanged", sizeChangedHandler);
	}
	
	//--------------------------------------------------------------------------
	//
	//  Constants
	//
	//--------------------------------------------------------------------------
	
	private static const DEFAULT_PERCENT_RADIUS:Number = 0.4;
	
	private static const MOUSE_CORRECTION:Number = 0.05;
	private static const ROTATION_SLOWDOWN:Number = 0.98;
	private static const ROTATION_THRESHOLD:Number = 0.001;
	
	private static const TAG_EVENTS:Object = {
		click : TagEvent.TAG_CLICK,
		rollOut : TagEvent.TAG_ROLL_OUT,
		rollOver : TagEvent.TAG_ROLL_OVER
	};
	
	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------
	
	private var center:Point = new Point();
	private var focalLength:Number;
	private var mouseCorrection:Number;
	private var movieClip:MovieClip = new MovieClip();
	private var rotations:Point = new Point();
	private var tags:Array /* of Tag */ = [];
	private var textFormat:TextFormat = new TextFormat();
	private var stopAnimate:Boolean = false;
	
	//--------------------------------------------------------------------------
	//
	//  Overridden properties
	//
	//--------------------------------------------------------------------------
	
	//--------------------------------------------------------------------------
	//
	//  Properties
	//
	//--------------------------------------------------------------------------
	
    //----------------------------------
    //  percentRadius
    //----------------------------------
	
	private var _percentRadius:Number = DEFAULT_PERCENT_RADIUS;
	
	[Inspectable(environment="none")]
	
	public function get percentRadius():Number
	{
		return _percentRadius * 200;
	}
	
	public function set percentRadius(value:Number):void
	{
		_percentRadius = value / 200;
	}
	
    //----------------------------------
    //  radius
    //----------------------------------
    
	private var _radius:Number;
	
    [Inspectable(category="General")]
    [PercentProxy("percentRadius")]
	
	public function get radius():Number
	{
		return _radius ? _radius : Math.min(width, height) * _percentRadius;
	}
	
	public function set radius(value:Number):void
	{
		_radius = value;
	}
	
    //----------------------------------
    //  weightField
    //----------------------------------
	
	private var _weightField:String;
	
	public function get weightField():String
	{
		return _weightField;
	}
	
	public function set weightField(value:String):void
	{
		_weightField = value;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Overridden methods
	//
	//--------------------------------------------------------------------------
	
	//--------------------------------------------------------------------------
	//
	//  Methods
	//
	//--------------------------------------------------------------------------
	
	private function computeRotations():void
	{
		// check if the mouse is inside the component
		if (getRect(this).contains(mouseX, mouseY))
		{
			// initialize new rotations
			rotations.x = (mouseY - center.y) * mouseCorrection;
			rotations.y = (center.x - mouseX) * mouseCorrection;
		}
		else
		{
			// rotations slow down
			rotations.x *= ROTATION_SLOWDOWN;
			rotations.y *= ROTATION_SLOWDOWN;
		}
	}
	
	private function createTags():void
	{
		// remove non existing tags
		removeTags();
		
		// create new tags
		var n:int = dataProvider.length;
		for (var i:int; i < n; i++)
		{
			var tag:Tag = new Tag(dataProvider[i], textFormat);
			
			tag.text = itemToLabel(dataProvider[i]);
			tag.backgroundColor = 0xFFCC00;
			
			for (var mouseEvent:String in TAG_EVENTS)
			{
				tag.addEventListener(mouseEvent, tagMouseEventHandler);
			}
			
			tags.push(tag);
			movieClip.addChild(tag);
		}
		
		positionTags();
	}
	
	private function positionTags():void
	{
		// see http://blog.massivecube.com/?p=9
		
		// distibution loop
		var n:int = tags.length;
		for (var i:int = 0; i < n; i++)
		{
			// spherical coordinates
			var phi:Number = Math.acos(-1 + (2 * i + 1) / n);
			var theta:Number = Math.sqrt(n * Math.PI)*phi;
			
			// coordinate conversion
			tags[i].cx = Math.cos(theta) * Math.sin(phi);
			tags[i].cy = Math.sin(theta) * Math.sin(phi);
			tags[i].cz = Math.cos(phi);
		}
	}
	
	private function removeTags():void
	{
		
		var n:int = tags.length;
		for (var i:int = 0; i < n; i++)
		{
			var tag:Tag = tags.pop();
			movieClip.removeChild(tag);
		}
	}
	
	private function rotateTags():void
	{
		// see http://www.kirupa.com/developer/actionscript/rotation_center.htm
		
		// check whether the sphere is still rotating
		var bRotating:Boolean = Math.abs(rotations.x) > ROTATION_THRESHOLD || 
		                        Math.abs(rotations.y) > ROTATION_THRESHOLD;
		
		if (bRotating)
		{
			var sx:Number = Math.sin(rotations.x);
			var cx:Number = Math.cos(rotations.x);
			var sy:Number = Math.sin(rotations.y);
			var cy:Number = Math.cos(rotations.y);
		}
		
		// transformation loop
		var n:int = tags.length;
		for (var i:int = 0; i < n; i++)
		{
			var tag:Tag = tags[i];
			
			if (bRotating)
			{
				// rotation around x
				var xy:Number = cx * tag.cy - sx * tag.cz;
				var xz:Number = sx * tag.cy + cx * tag.cz;
				
				// rotation around y
				var yz:Number = cy * xz - sy * tag.cx;
				var yx:Number = sy * xz + cy * tag.cx;
				
				// new 3D coordinates
				tag.cx = yx;
				tag.cy = xy;
				tag.cz = yz;
			}
			
			// scaling
			var scaleFactor:Number = 
				focalLength / (focalLength + tag.cz * radius);
			
			tag.scaleX = scaleFactor;
			tag.scaleY = scaleFactor;
			
			//alpha
			tag.alpha = Math.min(1, scaleFactor);
			
			// 2D projection
			scaleFactor *= radius;
			tag.x = tag.cx * scaleFactor + center.x;
			tag.y = tag.cy * scaleFactor + center.y;
		}
	}
	
	private function sortTags():void
	{
		// sort the array first
		tags.sortOn("cz", Array.DESCENDING|Array.NUMERIC);
		
		// arrange children according to the array
		var n:int = tags.length;
		for (var i:int = 0; i < n; i++)
		{
			// put the tag in the right place
			movieClip.setChildIndex(tags[i], i);
			
			// check if the tag is active
			var current:Number = tags[i].active ? i : current;
		}
		
		// put the active tag in the front if any
		if ( !isNaN(current) )
			movieClip.setChildIndex(tags[current], n-1);
	}
	
	//--------------------------------------------------------------------------
	//
	//  Overridden event handlers
	//
	//--------------------------------------------------------------------------
	
	override protected function collectionChangeHandler(e:Event):void
	{
		// run the default collectionChange handler
		super.collectionChangeHandler(e);
		
		createTags();
	}
	
	//--------------------------------------------------------------------------
	//
	//  Event handlers
	//
	//--------------------------------------------------------------------------
	
	private final function creationCompleteHandler(e:Event):void
	{
		/* It currently does nothing. The itemToLabel method returns the default
		 * value (label) without this creationComplete handling. I need to
		 * investigate why sometimes labelField isn't set yet when the
		 * collectionChange handler is called.
		 */
	}
	
	protected function doStopAnimate( e:Event ):void
	{
		stopAnimate = true;
	}
	
	protected function doStartAnimate( e:Event=null ):void
	{
		stopAnimate = false;
	}
	
	protected function enterFrameHandler(e:Event):void
	{
		if( !stopAnimate )
			animate();
	}
	
	protected function animate():void
	{
		computeRotations();
		rotateTags();
		sortTags();
	}	
		
	protected function sizeChangedHandler(e:Event):void
	{
		// center of the component
		center.x = width * 0.5;
		center.y = height * 0.5;
		
		// value used for scaling
		focalLength = Math.min(width, height);
		
		// mouse correction depends on radius
		// radius may depend on the new size
		mouseCorrection = MOUSE_CORRECTION / radius;
		
		// avoids overflow of the sprites
		scrollRect = new Rectangle(0, 0, width, height);
	}
	
	private final function tagMouseEventHandler(e:Event):void
	{
		// get the mouse event properties
		var type:String = TAG_EVENTS[e.type];
		var data:Object = e.currentTarget.data;
		
		doStartAnimate();
		
		// dispatch a tag event
		dispatchEvent( new TagEvent(type, data) );
	}
}

}